iT邦幫忙

2026 iThome 鐵人賽

DAY 15
0

在前兩週,我們依序完成了文字搜尋、AST 語法索引、直接 import 追蹤、向量嵌入以及基於 RRF 的 Hybrid Search。

但如果你問一個工程問題:「加上向量與 RRF 之後,檢索結果真的有變好嗎?好多少?」

很多人優化 RAG 的方式是:在終端機隨便敲兩句「Qdrant 設定在哪」,看到螢幕跳出滿意的結果,就宣布「檢索效果大幅提升」。

這不叫評估,這叫倖存者偏差。
在真實世界裡,往往你為了解決問題 A 調大了向量權重,原本答得很好的問題 B 卻被擠出了 Top 3。

今天進入第三週的第一天,我們要建立一份真正能用程式重跑、完全客觀的評估資料集(Evaluation Dataset),把 Day 3 列出的願景正式轉化為系統隨時可跑的基準測試。

為什麼答案不能只寫在 Markdown 裡?

在 Day 3 我們雖然列了 15 題驗收題目,但如果題目只保存在文章或 Markdown 條列裡,每次改了程式碼,你就得「人眼逐行檢查」。

評估資料集必須滿足三個工程鐵律:

  1. 機器可讀(Machine-Readable):使用結構化的 JSON 格式,評估程式能自動批次執行。

  2. 黃金標準明確(Ground Truth / Expected Target):精準標註每個查詢預期必須命中的「檔案相對路徑」與「相關 Symbol 名稱」。

  3. 分群分類(Categorized):涵蓋不同難度的問題類型,才能清楚看出不同檢索管道在各題型上的長處與短板。

評估資料格式設計:data/eval_cases.json

我們在專案自己的 data/ 目錄建立測試集,先涵蓋最常見的三種實戰情境:

  • exact_symbol:已知精準名稱的定位題(測試 Lexical / Symbol Index 的準確率)。

  • dependency:模組引用與依賴題(測試 Import 關係檢索)。

  • semantic_intent:不知道精確名稱、偏向自然語言描述的概念題(測試 Vector / Hybrid 檢索)。

[
  {
    "id": "eval_01",
    "category": "exact_symbol",
    "query": "COLLECTION_NAME 在哪裡設定?",
    "expected_paths": [
      "src/rag_common.py"
    ],
    "expected_symbols": [
      "COLLECTION_NAME"
    ]
  },
  {
    "id": "eval_02",
    "category": "dependency",
    "query": "哪些檔案直接 import rag_common?",
    "expected_paths": [
      "src/build_index.py",
      "src/chat_reranker.py",
      "src/chat_reranker_guarded.py",
      "src/rag_chat.py"
    ],
    "expected_symbols": []
  },
  {
    "id": "eval_03",
    "category": "semantic_intent",
    "query": "Qdrant 本機路徑在哪裡設定?",
    "expected_paths": [
      "src/rag_common.py",
      "src/build_index.py"
    ],
    "expected_symbols": [
      "QDRANT_LOCAL_DIR"
    ]
  },
  {
    "id": "eval_04",
    "category": "exact_symbol",
    "query": "建立索引的實作在哪裡?",
    "expected_paths": [
      "src/build_index.py"
    ],
    "expected_symbols": [
      "build_index"
    ]
  },
  {
    "id": "eval_05",
    "category": "semantic_intent",
    "query": "對話問答的進入點在哪個檔案?",
    "expected_paths": [
      "src/rag_chat.py"
    ],
    "expected_symbols": [
      "chat_loop"
    ]
  }
]

實作資料集驗證器:app/evaluation.py

在撰寫指標計算法之前,先建立一個載入與驗證資料集的資料類別(Data Class),確保資料格式完備:

# app/evaluation.py
import json
from dataclasses import dataclass
from pathlib import Path
from typing import List

@dataclass
class EvalCase:
    id: str
    category: str
    query: str
    expected_paths: List[str]
    expected_symbols: List[str]

class EvalDatasetLoader:
    @classmethod
    def load_from_json(cls, file_path: str = "data/eval_cases.json") -> List[EvalCase]:
        path = Path(file_path)
        if not path.exists():
            raise FileNotFoundError(f"找不到評估資料集: {file_path}")

        raw_data = json.loads(path.read_text(encoding="utf-8"))
        cases = []
        for item in raw_data:
            cases.append(EvalCase(
                id=item["id"],
                category=item["category"],
                query=item["query"],
                expected_paths=item.get("expected_paths", []),
                expected_symbols=item.get("expected_symbols", [])
            ))
        return cases


實作單元測試:tests/unit/test_eval_loader.py

驗證評估資料能否被正確解析,並確保至少包含上述三種核心題型:

# tests/unit/test_eval_loader.py
import json
from app.evaluation import EvalDatasetLoader

def test_eval_dataset_loading(tmp_path):
    dataset_file = tmp_path / "eval_test.json"
    dummy_cases = [
        {
            "id": "test_1",
            "category": "exact_symbol",
            "query": "Where is COLLECTION_NAME?",
            "expected_paths": ["src/rag_common.py"],
            "expected_symbols": ["COLLECTION_NAME"]
        }
    ]
    dataset_file.write_text(json.dumps(dummy_cases), encoding="utf-8")

    loaded = EvalDatasetLoader.load_from_json(str(dataset_file))
    assert len(loaded) == 1
    assert loaded[0].id == "test_1"
    assert loaded[0].category == "exact_symbol"
    assert "src/rag_common.py" in loaded[0].expected_paths

執行測試確認綠燈:

uv run pytest tests/unit/test_eval_loader.py -v

tests/unit/test_eval_loader.py::test_eval_dataset_loading PASSED           [100%]
============================== 1 passed in 0.04s ==============================

實際驗收評估資料集

在終端機檢查正式的評估資料集規模:

uv run python -c "from app.evaluation import EvalDatasetLoader; cases = EvalDatasetLoader.load_from_json('data/eval_cases.json'); print(f'已載入 {len(cases)} 題測試案例')"

輸出結果:

已載入 5 題測試案例

總結與下一步

今天我們完成了「評估工程化」的第一步:

  1. 測試標準代碼化:不再把考題當成文章邊角料,而是做成可重複運行的 eval_cases.json。

  2. 定義黃金標準(Ground Truth):為每一個查詢鎖定預期命中的相對路徑與符號,消滅打分數時的人為模糊空間。

現在考卷已經出好了,但我們該如何為檢索結果打分數?如果命中結果排在第 1 名跟排在第 5 名,分數該怎麼算?

明天,我們將實作檢索評估的三大核心量化指標:Hit@k、MRR(Mean Reciprocal Rank) 與 Precision@k,為搜尋品質打造真正的量尺!


上一篇
Day 14:第二週收尾:Deterministic Reranking 與兩週檢索地基總驗收
下一篇
Day 16:用數據說話:實作檢索品質三大量化指標
系列文
30 天打造 Codebase Intelligence Agent:從程式碼檢索、結構化索引到變更影響分析實戰 共 17 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言